class_name UnitBase
extends CharacterBody2D

# move
@export_category("Move")
@export var speed: float = 40.0
@export var gravity: float = 300
@export var water_speed_scale: float = 0.5
@export var water_gravity_scale: float = 0.5
@export var numb_speed_scale: float = 0.5
var is_in_water: bool = false
var normal_speed: float
var normal_approach_speed: float 
var normal_gravity: float

# states
@export_category("States")
@onready var states: StateMachine = $States
@export var init_state: String = "Wander"

# hit
var current_hit_data: HitData = null
var is_dead: bool = false
@onready var hit_area: Area2D = $ViewRoot/HitArea
@onready var hit_collision_shape_2d: CollisionShape2D = $ViewRoot/HitArea/CollisionShape2D
@onready var hit_cooldown_timer: Timer = $ViewRoot/HitArea/HitCooldownTimer
@onready var hurt_area: Area2D = $ViewRoot/HurtArea
@onready var hurt_collision_shape: CollisionShape2D = $ViewRoot/HurtArea/CollisionShape2D
@onready var light_animation_player: AnimationPlayer = $Light/LightAnimationPlayer
@onready var light_parent: Node2D = $Light

# unit data
var unit_data: UnitBaseData = null

# level
var level: Level = null

# push drop item
@export var push_drop_item_power: float = 10

# viewroot
@onready var view_root: Node2D = $ViewRoot

# navigation
@onready var navigation_agent_2d: NavigationAgent2D = $NavigationAgent2D
var attack_target: Node2D = null

# approach
@export_category("Approach")
@export var approach_speed: float = 60
@onready var quit_approach_timer: Timer = $QuitApproachTimer

# search
@export_category("Search")
@onready var search_area: Area2D = $ViewRoot/SearchArea
@onready var search_sense_range_shape: CollisionShape2D = $ViewRoot/SearchArea/CollisionShape2D

# wander
@export_category("Wander")
@export var wander_width: float = 200.0   # 徘徊矩形的宽度
@export var wander_height: float = 120.0  # 徘徊矩形的高度
@export var change_dir_time: float = 2.0  # 随机切换目标时间
var home_position: Vector2
var target_pos: Vector2
var timer: float = 0.0

# body collision
@onready var body_collision_shape_2d: CollisionShape2D = $BodyCollisionShape2D

# push drop item
@onready var push_drop_item_area: Area2D = $ViewRoot/PushDropItemArea
@onready var push_collision_shape_2d: CollisionShape2D = $ViewRoot/PushDropItemArea/CollisionShape2D

# animation
@export_category("Animation")
@onready var animation_player: AnimationPlayer = $AnimationPlayer

# side
var is_enemy: bool = false

# buff
@onready var buff_parent: Marker2D = $BuffParent

# stick
@onready var stick_parent: Node2D = $ViewRoot/StickParent

# drop
var is_now_on_floor: bool = true
var is_last_on_floor: bool = true
var jump_start_height: float = 0

# raycastpoint
@onready var ray_cast_point: Marker2D = $ViewRoot/RayCastPoint

# particle parent
@onready var particle_parent: Marker2D = $ParticleParent


func _ready() -> void:
	remember_init_speed_gravity()
	states.transition(init_state)
	

func remember_init_speed_gravity():
	normal_gravity = gravity
	normal_speed = speed
	normal_approach_speed = approach_speed


func take_hit(hit_data: HitData, source: Node2D = null):
	if is_dead: return
	
	if source:
		var is_source_partner: bool = false
		if source is Player and not self.is_enemy:
			is_source_partner = true
			
		if not is_source_partner:
			attack_target = source
		
	current_hit_data = hit_data
	
	if hit_data.direct_dead:
		current_hit_data.actual_damage = unit_data.hp
		unit_data.hp = 0
	elif hit_data.is_direct_damage:
		current_hit_data.actual_damage = hit_data.direct_damage
		unit_data.hp -= hit_data.direct_damage
	else:
		var damage = get_damage(current_hit_data.attack)
		unit_data.hp -= damage
		current_hit_data.actual_damage = damage
		
	if unit_data.hp <= 0:
		unit_data.hp = 0
		is_dead = true
		level.clear_dead_unit(unit_data)
		
	states.transition("Hit")
	AudioManager.play_sound("sfx_hit")


func get_damage(attack: int):
	var result = attack - unit_data.get_base_defense()
	if result <= 1: result = 1
	
	return result
	

func death_fade():
	var tween_alpha = create_tween()
	tween_alpha.tween_property(self, "modulate:a", 0, 0.5) \
	.set_trans(Tween.TRANS_SINE) \
	.set_ease(Tween.EASE_OUT)
	
	await tween_alpha.finished
	queue_free()
	

func get_face():
	return view_root.scale.x
	

func set_face(face):
	view_root.scale.x = face
	

func disable_body_collision():
	body_collision_shape_2d.disabled = true
	
	
func enable_body_collision():
	body_collision_shape_2d.disabled = false


func process_push_drop_item():
	var bodies = push_drop_item_area.get_overlapping_bodies()
	if bodies.size() <= 0: return
	
	for body in bodies:
		if body is RigidBody2D:
			var target = body as RigidBody2D
			if target.can_push:
				target.apply_impulse(velocity.normalized() * push_drop_item_power)
	

func start_summon_creature_life_coundown():
	await get_tree().create_timer(unit_data.summon_source_item.summon_creature_max_life_time).timeout
	states.transition("Death")
	
	
func pick_new_wander_target():
	pass


func process_wander(delta: float):
	timer -= delta
	if timer <= 0.0 or global_position.distance_to(target_pos) < 5.0:
		pick_new_wander_target()
	
	navigation_agent_2d.target_position = target_pos
		
	if navigation_agent_2d.is_navigation_finished():
		return
	
	var next_pos = navigation_agent_2d.get_next_path_position()
	var dir = (next_pos - global_position).normalized()
	var target_velocity = dir * speed
	if navigation_agent_2d.avoidance_enabled:
		navigation_agent_2d.set_velocity(target_velocity)
	else:
		_on_navigation_agent_2d_velocity_computed(target_velocity, delta)
	
	
func process_face():
	if attack_target:
		var dx = attack_target.global_position.x - global_position.x
		if abs(dx) > 5:
			view_root.scale.x = 1 if dx > 0 else -1
	elif abs(velocity.x) > 5:
		view_root.scale.x = 1 if velocity.x > 0 else -1
		
	
func process_search():
	var target_areas = search_area.get_overlapping_areas()
	if target_areas.size() <= 0: return
	
	var min_distance: float = INF
	var min_distance_target: Node2D = null
	for area in target_areas:
		var result = GlobalUtils.raycast_to(ray_cast_point.global_position, area.source.ray_cast_point.global_position, 1)
		if result: continue
		
		var distance = ray_cast_point.global_position.distance_to(area.source.global_position)
		
		var source = area.source
		if source is Player:
			source = source as Player
			var autual_search_radius = search_sense_range_shape.shape.radius - source.player_data.get_anti_search()
			if distance > autual_search_radius:
				continue
		
		if distance < min_distance:
			min_distance = distance
			min_distance_target = area.source
	
	if min_distance_target:
		attack_target = min_distance_target
		states.transition("Approach")
		

func set_attack_target(target: Node2D):
	attack_target = target
	states.transition("Approach")
	

func set_search_radius(radius: float):
	var shape = search_sense_range_shape.shape as CircleShape2D
	shape.radius = radius
	

func process_approach(delta: float):
	if not attack_target: return
	
	if "approach_target" in attack_target:
		navigation_agent_2d.target_position = attack_target.approach_target.global_position
	else:
		navigation_agent_2d.target_position = attack_target.global_position
		
	if navigation_agent_2d.is_navigation_finished():
		return
	
	var next_pos = navigation_agent_2d.get_next_path_position()
	var dir = (next_pos - global_position).normalized()
	var target_velocity = dir * approach_speed
	if navigation_agent_2d.avoidance_enabled:
		navigation_agent_2d.set_velocity(target_velocity)
	else:
		_on_navigation_agent_2d_velocity_computed(target_velocity, delta)
		
	
func _on_navigation_agent_2d_velocity_computed(safe_velocity: Vector2, delta: float = 0.0) -> void:
	velocity = velocity.lerp(safe_velocity, 0.05)
	move_and_slide()
	
	# test collision with player
	if is_enemy:
		avoid_collider_with_player_body()


func avoid_collider_with_player_body():
	if not level.player: return
	
	var player_body_rect = level.player.get_body_collision_rect()
	var self_body_rect = get_body_collision_rect()
	self_body_rect = self_body_rect.grow(0.5)
	
	if self_body_rect.intersects(player_body_rect):
		var reverse_dir = (ray_cast_point.global_position - level.player.ray_cast_point.global_position).normalized()
		velocity = reverse_dir * speed
	

func get_body_collision_rect():
	var shape = body_collision_shape_2d.shape as RectangleShape2D
	return Rect2(body_collision_shape_2d.global_position - shape.size / 2.0,shape.size)
			

func process_hit():
	if not hit_cooldown_timer.is_stopped(): return
	
	var areas = hit_area.get_overlapping_areas()
	if areas.size() <= 0: return
	
	for area in areas:
		var source = area.source
		var hit_data = get_hit_data()
		source.take_hit(hit_data, self)
		
		if self.has_method("on_hit"):
			self.call("on_hit", source)
		
		#if source is Player:
			#source = source as Player
			#var dir = sign(ray_cast_point.global_position.y - source.ray_cast_point.global_position.y) 
			#velocity.y = dir * speed
	
	hit_cooldown_timer.stop()
	hit_cooldown_timer.start()
	

func get_hit_data():
	var config = unit_data.config as UnitBaseConfig
	
	var critical_rate = config.base_critical_attack_rate
	var is_critical = randf() <= critical_rate 
	
	var hit_data = HitData.new()
	hit_data.is_critical = is_critical
	hit_data.attack = config.base_attack if not is_critical else config.base_attack * (1.0 + config.base_critical_attack_bonus)
	hit_data.knockback = config.base_knockback
	
	return hit_data


func set_is_enemy(is_enemy: bool):
	self.is_enemy = is_enemy
	
	if is_enemy:
		hit_area.set_collision_mask_value(7, true)
		hit_area.set_collision_mask_value(6, false)
		hurt_area.set_collision_layer_value(6, true)
		hurt_area.set_collision_layer_value(7, false)
		search_area.set_collision_mask_value(7, true)
		search_area.set_collision_mask_value(6, false)
	else:
		hit_area.set_collision_mask_value(7, false)
		hit_area.set_collision_mask_value(6, true)
		hurt_area.set_collision_layer_value(6, false)
		hurt_area.set_collision_layer_value(7, true)
		search_area.set_collision_mask_value(7, false)
		search_area.set_collision_mask_value(6, true)


func play_wander_animation():
	pass
	

func process_quit_approach():
	if not attack_target: 
		states.transition("Wander")
		return
		
	if is_attack_target_out_of_senserange():
		if quit_approach_timer.is_stopped():
			quit_approach_timer.start()
	else:
		if not quit_approach_timer.is_stopped():
			quit_approach_timer.stop()
			

func is_attack_target_out_of_senserange():
	var dist = attack_target.global_position.distance_to(global_position)
	var shape = search_sense_range_shape.shape as CircleShape2D
	return dist > shape.radius
			

func _on_quit_approach_timer_timeout() -> void:
	attack_target = null
	states.transition("Wander")
	quit_approach_timer.stop()
	

func init_by_data(data: UnitBaseData):
	unit_data = data as UnitBaseData
	
	global_position = unit_data.pos
	set_face(unit_data.face)
	set_is_enemy(unit_data.is_enemy)
	home_position = unit_data.home_position
	
	if unit_data.summon_source_item:
		start_summon_creature_life_coundown()
		
	var unit_base_config = data.config as UnitBaseConfig
	if unit_base_config.speed:
		speed = unit_base_config.speed
	if unit_base_config.approach_speed:
		approach_speed = unit_base_config.approach_speed
	if unit_base_config.search_range:
		var shape = search_sense_range_shape.shape as CircleShape2D
		shape.radius = unit_base_config.search_range
		
	remember_init_speed_gravity()
	
	if self.has_method("pick_new_wander_target"):
		pick_new_wander_target()
	
	
func collect_data():
	unit_data.pos = global_position
	unit_data.face = get_face()
	

func get_random_hurt_position():
	var shape = hurt_collision_shape.shape as RectangleShape2D
	var rand_x_offset = randf_range(-shape.size.x / 2.0, shape.size.x / 2.0)
	var rand_y_offset = randf_range(-shape.size.y / 2.0, shape.size.y / 2.0)
	var offset = Vector2(rand_x_offset, rand_y_offset)
	
	var target_position = hurt_collision_shape.global_position + offset
	return target_position
	

func pause_animation():
	animation_player.pause()
	

func death(drop_item: bool = true):
	disable_hit_collision()
	disable_hurt_collision()
	disable_body_collision()
	level.emit_particle("particle_death_effect", get_random_hurt_position())
	pause_animation()
	death_fade()
	
	if drop_item and unit_data.is_enemy:
		level.start_drop(unit_data.config.drop_config, global_position)
		

func process_gravity(delta):
	if not is_on_floor():
		velocity.y += gravity * delta
		
	
func enter_water():
	is_in_water = true
	speed = normal_speed * water_speed_scale
	gravity = normal_gravity * water_gravity_scale
	
	
func exit_water():
	is_in_water = false
	speed = normal_speed 
	gravity = normal_gravity
	

func process_speed_affect():
	var target_speed: float = normal_speed
	var target_gravity: float = normal_gravity
	var target_approach_speed: float = normal_approach_speed
	
	# is in water
	var is_in_water: bool = false
	for water_rect in level.all_water_rects:
		if water_rect.has_point(ray_cast_point.global_position):
			is_in_water = true
			break
	
	if is_in_water:
		is_in_water = true
		target_speed = target_speed * water_speed_scale
		target_gravity = target_gravity * water_gravity_scale
		target_approach_speed = target_approach_speed * water_gravity_scale
	else:
		is_in_water = false
	
	var debuff_numb = get_buff_by_name("debuff_numb")
	if debuff_numb:
		target_speed = target_speed * debuff_numb.numb_speed_scale
		target_approach_speed = target_approach_speed * debuff_numb.numb_speed_scale
		
	var debuff_frozen = get_buff_by_name("debuff_frozen")
	if debuff_frozen:
		target_speed = target_speed * debuff_frozen.frozen_speed_scale
		target_approach_speed = target_approach_speed * debuff_frozen.frozen_speed_scale
	
	speed = target_speed
	approach_speed = target_approach_speed
	gravity = target_gravity


func walk_cam_shake():
	if not unit_data.config.walk_cam_shake: return
	
	var distance_2_cam_center = global_position.distance_to(level.camera.global_position)
	if distance_2_cam_center >  unit_data.config.walk_cam_shake_max_distance:
		return
	
	var dist_diff = abs(unit_data.config.walk_cam_shake_max_distance - distance_2_cam_center) 
	var rate = dist_diff * 1.0 / unit_data.config.walk_cam_shake_max_distance
	
	var target_cam_shake_intensity = rate * unit_data.config.walk_cam_shake_max_intensity
	level.camera.start_shake(target_cam_shake_intensity, unit_data.config.walk_cam_shake_duration)
	

func is_colliding_now(body: CharacterBody2D) -> bool:
	return body.test_move(body.transform, Vector2.ZERO)
	

func place_safely_hotizontal(unit: CharacterBody2D):
	var step := -Vector2(unit.velocity.x, 0).normalized() * 2
	var max_checks := 20

	for i in range(max_checks):
		if not is_colliding_now(unit):
			break
		unit.global_position += step

	
func disable_hurt_collision():
	hurt_collision_shape.disabled = true


func enable_hurt_collision():
	hurt_collision_shape.disabled = false
	

func disable_hit_collision():
	hit_collision_shape_2d.disabled = true


func enable_hit_collision():
	hit_collision_shape_2d.disabled = false


func remove_buff(buff_name: String):
	var buff_inst = get_buff_by_name(buff_name)
	if buff_inst:
		buff_inst.on_remove()
		buff_parent.remove_child(buff_inst)
		buff_inst.queue_free()


func add_buff(buff_name: String, sender: Node2D):
	var buff_inst = (ResourceManager.get_resource(buff_name) as PackedScene).instantiate()
	buff_inst.holder = self
	buff_parent.add_child(buff_inst)
	buff_inst.sender = sender
	buff_inst.name = buff_name
	return buff_inst
	

func get_buff_by_name(buff_name: String):
	for index in range(buff_parent.get_child_count()):
		var buff = buff_parent.get_child(index)
		if buff.name == buff_name:
			return buff
	return null


func set_velocity_forward(speed: float):
	velocity.x = get_face() * speed
	

func set_velocity_y(vel: float):
	velocity.y = vel
	

func process_drop_damage():
	if gravity <= 0: return
	
	if is_in_water: return
	
	is_now_on_floor = is_on_floor()
	
	if not is_last_on_floor and is_now_on_floor:
		# just grounded:
		var now_position_y = global_position.y
		var diff = now_position_y - jump_start_height
		if diff > 0 and diff > unit_data.config.safe_drop_height:
			var rate = (diff - unit_data.config.safe_drop_height) * 1.0 / (unit_data.config.death_drop_height * 1.0 - unit_data.config.safe_drop_height)
			
			var hit_data = HitData.new()
			hit_data.is_critical = true
			hit_data.is_direct_damage = true
			hit_data.direct_damage = unit_data.max_hp * rate
			hit_data.knockback = 300
			
			if self is Player and diff < unit_data.config.death_drop_height and GlobalData.find_item_by_name("棒棒糖"):
				hit_data.direct_damage = 0
			
			if self is Player and GlobalData.find_item_by_name("棉花糖"):
				if unit_data.hp <= hit_data.direct_damage:
					hit_data.direct_damage = unit_data.hp - 1
			
			take_hit(hit_data)
	
	if is_last_on_floor and not is_now_on_floor:
		# just jump
		jump_start_height = global_position.y
		
	is_last_on_floor = is_now_on_floor


func _physics_process(delta: float) -> void:
	process_drop_damage()
	
	if GameManager.frame_counter % 2 == 0:
		process_speed_affect()


func heal(heal_point: float):
	unit_data.hp += heal_point
	if unit_data.hp > unit_data.max_hp:
		unit_data.hp = unit_data.max_hp
		
	if self is Player:
		GlobalSignals.player_info_changed.emit()
	
	var effect = (ResourceManager.get_resource("particle_heal") as PackedScene).instantiate()
	particle_parent.add_child(effect)
	
	level.raise_damage_number(heal_point, get_random_hurt_position() + Vector2.UP * 16, 1.5, Color("fff12b"))
		
		
func disable_physics():
	if is_dead: return
	
	search_sense_range_shape.disabled = true
	hit_collision_shape_2d.disabled = true
	hurt_collision_shape.disabled = true
	push_collision_shape_2d.disabled = true
	body_collision_shape_2d.disabled = true
	light_parent.visible = false
	

func enable_physics():
	if is_dead: return
	
	search_sense_range_shape.disabled = false
	hit_collision_shape_2d.disabled = false
	hurt_collision_shape.disabled = false
	push_collision_shape_2d.disabled = false
	body_collision_shape_2d.disabled = false
	light_parent.visible = true
		
